<?php namespace Reezy\APIToken;


use Psr\Http\Message\ServerRequestInterface;
use Psr\SimpleCache\CacheInterface;
use Psr\SimpleCache\InvalidArgumentException;
use function strpos;
use function substr;
use function time;


class APIToken
{

    public static function get(ServerRequestInterface $request): APIToken
    {
        return $request->getAttribute(APIToken::class);
    }

    public static function getUserId(ServerRequestInterface $request): int
    {
        return (int)$request->getAttribute('userId');
    }

    private $tokenKey;
    private $freshTime;
    private $ttl;
    private $name;

    private $secretKey;

    /**
     * @var CacheInterface
     */
    private $cache;

    public function __construct(string $name, array $config, CacheInterface $cache)
    {
        $this->name = $name;
        $this->tokenKey = $config['token_key'] ?? 'token';
        $this->freshTime = $config['freshTime'] ?? 7200;
        $this->ttl = $config['ttl'] ?? 86400;
        $this->secretKey = $config['secret_key'];

        $this->cache = $cache;
    }

    /**
     * @param ServerRequestInterface $request
     * @param APITokenInfo $info
     * @return array
     * @throws InvalidArgumentException
     */
    public function check(ServerRequestInterface $request, APITokenInfo $info): array
    {
        $now = time();

        if ($now < $info->ts) {
            throw new APITokenException("token invalid");
        }
        if ($now > $info->ts + $this->ttl) {
            throw new APITokenException("token dead");
        }

        $payload = $this->getPayload($request);
        $hmac = APITokenUtil::hmac($this->name, $info->userId, $info->ts,  $payload, $this->secretKey);
        if ($hmac !== $info->sign) {
            throw new APITokenException("token verify error");
        }

        $cachedToken = $this->cache->get("api:$this->name:token:{{$info->userId}}");
        if ($cachedToken == null) {
            throw new APITokenException("token revoked");
        }
        if ($cachedToken == $info->token) {
            if ($now > $info->ts + $this->freshTime) {
                $newToken = APITokenUtil::encode($this->name, $info->userId, $now,  $payload, $this->secretKey);
                $this->cache->set("api:$this->name:token:{{$info->userId}}", $newToken, $this->ttl);
                $this->cache->set("api:$this->name:fresh:$info->sign", "1", 60);
                $this->cache->set("api:$this->name:stale:$info->sign", "1", 60);
                return [$newToken, $now];
            }
            return [null, $now];
        }
        if ($now < $info->ts + 60 && $this->cache->has("api:$this->name:fresh:$info->sign")) {
            return [null, $now];
        }
        if ($now < $info->ts + $this->freshTime) {
            throw new APITokenException("token kicked");
        }
        if (!$this->cache->has("api:$this->name:stale:$info->sign")) {
            throw new APITokenException("token staled");
        }
        return [null, $now];
    }


    /**
     * @param ServerRequestInterface $request
     * @param int $userId
     * @return string
     * @throws InvalidArgumentException
     */
    public function issue(ServerRequestInterface $request, int $userId): string
    {
        $payload = $this->getPayload($request);
        $token = APITokenUtil::encode($this->name, $userId, time(), $payload, $this->secretKey);
        $this->cache->set("api:$this->name:token:{{$userId}}", $token);
        return $token;
    }

    /**
     * @param int $userId
     * @throws InvalidArgumentException
     */
    public function revoke(int $userId)
    {
        $this->cache->delete("api:$this->name:token:{{$userId}}");
    }

    private function getPayload(ServerRequestInterface $request)
    {
        $deviceId = $request->getHeaderLine('device-id') ?? $request->getHeaderLine('deviceId') ?? '';
        $ua = $request->getHeaderLine('user-agent') ?? '';

        $start = strpos($ua, ' (');
        $end = strpos($ua, ') ');
        return substr($ua, $start + 2, $end - $start - 2) . $deviceId;
    }

}
